using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.Net;
using Caece.Libreria;
using Caece.Libreria.Protocolo;

namespace Caece.Libreria.DNS
{
	public class DNSStateObject
	{
		public Socket workSocket = null;
		public const int BufferSize = 1024;
		public byte[] buffer = new byte[BufferSize];
		public StringBuilder sb = new StringBuilder();
	}
	public class clsDNS
	{
		private const int COMMUNICATION_TIMEOUT = 1000;
		private int maxProceso;
		private List<clsInfo> frontEnds;
		private Thread listeningThread;
		private Thread actualizaListaThread;
		private Socket conexion;
		private clsInfo cProximo = new clsInfo();
		private int PuertoDNS;
		private int timeOut;
		public static ManualResetEvent allDone = new ManualResetEvent(false);


		public clsDNS(int PuertoDNS, int timeOut)
		{
			this.PuertoDNS = PuertoDNS;
			maxProceso = 0;
			frontEnds = new List<clsInfo>();
			this.timeOut = timeOut;

			///*Sacar*/
			//clsInfo cnodo = new clsInfo(new byte[] {192,168,1,102},11001,1);
			//frontEnds.Add(cnodo);
			//clsInfo cnodo1 = new clsInfo(new byte[] { 192, 168, 1, 102 }, 11002, 2);
			//frontEnds.Add(cnodo1);

			//maxProceso = 2;


			///*Sacar*/
		}

		public void Start()
		{
			this.InicDNSThread();
			this.IniciarListeningThread();
			Util.Log("DNS Server started");
			Util.Log("Listening for incomming requests...");
		}

		public void Stop()
		{
			//TODO: implementar un Stop para poder simular la caida del DNS
			//FinDNSThread();
			this.FinListeningThread();
		}

		private List<clsInfo> ListaMayores(clsInfo cNodo)
		{

			List<clsInfo> listaMay = new List<clsInfo>();

			foreach (clsInfo cNodito in frontEnds)
			{
				if (cNodito.ProcesoId >= cNodo.ProcesoId)
				{
					listaMay.Add(cNodito);
				}
			}

			return listaMay;
		}



		private void SetearProximo()
		{
			lock (cProximo)
			{
				if (this.cProximo.ProcesoId == 0)
				{
					this.cProximo = this.frontEnds[0];
				}
				else
				{
					clsInfo[] copia = this.frontEnds.ToArray();

					for (int i = 0; i < copia.GetLength(0); i++)
					{
						if (copia[i].ProcesoId == cProximo.ProcesoId)
						{
							if (copia[i].ProcesoId < copia.GetLength(0))
							{
								this.cProximo = copia[i + 1];
							}
							else
							{
								this.cProximo = copia[0];
							}
							break;
						}
					}
				}
			}
		}


		private int AgregarServidor(byte[] pIp, int pPuerto)
		{
			clsInfo info;
			int ret = 0;

			lock (frontEnds)
			{

				maxProceso++;

				info = new clsInfo(pIp, pPuerto, maxProceso);
				if (frontEnds.Contains(info))
					ret = info.ProcesoId;
				else
				{
					frontEnds.Add(info);
					ret = maxProceso;
				}
			}
			return ret;

		}

		public List<clsInfo> ObtenerLista()
		{
			return this.frontEnds;

		}

		private void ActualizarLista()
		{
			byte[] RecibirBytes = new byte[256];
			byte[] DatosBytes = new byte[256];
			String salida;



			while (true)
			{
				Array copia = this.frontEnds.ToArray();
				foreach (clsInfo cNodo in copia)
				{
					try
					{
						conexion = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
						IPAddress dir = new IPAddress(cNodo.Ip);
					
						IPEndPoint DireccionDestino = new IPEndPoint(dir, cNodo.Puerto);
						DatosBytes = FrontEndProtocol.Encode(FrontEndProtocolCommand.AreYouAlive, "");

						conexion.Connect(DireccionDestino);
						conexion.SendTimeout = COMMUNICATION_TIMEOUT;
						conexion.Send(DatosBytes, DatosBytes.Length, SocketFlags.None);
						conexion.ReceiveTimeout = COMMUNICATION_TIMEOUT;
						conexion.Receive(RecibirBytes);
						FrontEndProtocol.Decode(RecibirBytes, out salida);
						Util.LogFormat("-> DNSProtocolCommand.AreYouAliveACK. Nodo:{0}", cNodo.ProcesoId);
					}					
					catch (SocketException se)
					{
						this.frontEnds.Remove(cNodo);
						Util.LogFormat("* SocketException : {0}", se.Message);
					}
					catch (Exception e)
					{
						Util.LogFormat("* Exception : {0}", e.Message);
					}
					finally
					{
						if (conexion.Connected)
							conexion.Shutdown(SocketShutdown.Both);
						conexion.Close();
					}
				}
				Thread.Sleep(this.timeOut);
			}
		}

		private void InicDNSThread()
		{
			actualizaListaThread = new Thread(ActualizarLista);
			//            actualizaListaThread.IsBackground = true;
			actualizaListaThread.Start();

		}

		private void FinDNSThread()
		{
			actualizaListaThread.Abort();
		}

		private void IniciarListeningThread()
		{
			listeningThread = new Thread(StartListening);
			listeningThread.IsBackground = true;
			listeningThread.Start();
		}

		private void FinListeningThread()
		{
			listeningThread.Abort();
		}

		private void StartListening()
		{
			byte[] bytes = new Byte[1024];

			//IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName());
			//IPAddress ipAddress = ipHostInfo.AddressList[0];
			IPAddress ipAddress = IPAddress.Any;
			IPEndPoint localEndPoint = new IPEndPoint(ipAddress, PuertoDNS);

			// Create a TCP/IP socket.
			Socket listener = new Socket(AddressFamily.InterNetwork,
			SocketType.Stream, ProtocolType.Tcp);


			try
			{
				listener.Bind(localEndPoint);
				listener.Listen(100);

				while (true)
				{
					allDone.Reset();
					listener.BeginAccept(new AsyncCallback(AcceptCallback), listener);
					allDone.WaitOne();
				}
			}
			catch (Exception e)
			{
				Console.WriteLine(e.ToString());
			}

		}

		private void AcceptCallback(IAsyncResult ar)
		{
			allDone.Set();

			Socket listener = (Socket)ar.AsyncState;
			Socket handler = listener.EndAccept(ar);

			// Create the state object.
			DNSStateObject state = new DNSStateObject();
			state.workSocket = handler;
			handler.BeginReceive(state.buffer, 0, DNSStateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state);
		}

		private void ReadCallback(IAsyncResult ar)
		{
			String content = String.Empty;
			byte[] ret = new byte[] { };
			byte[] ip = new byte[] { 0, 0, 0, 0 };


			DNSStateObject state = (DNSStateObject)ar.AsyncState;
			Socket handler = state.workSocket;
			int proceso = 0;
			int puerto = 0;
			clsInfo cNodo = new clsInfo();
			string[] datos;
			// Read data from the client socket. 
			int bytesRead = handler.EndReceive(ar);


			if (bytesRead > 0)
			{

				DNSProtocolCommand cmm = DNSProtocol.Decode(state.buffer, out content);
				//state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));
				//content = state.sb.ToString();


				switch (cmm)
				{
					case DNSProtocolCommand.NewFrontEnd:

						string[] data = content.Split(new char[] { '|' });
						if (data.Length > 0)
						{
							ip = DNSProtocol.StrToByteArray(data[1].ToString());
							puerto = Int32.Parse(data[0].ToString());

							proceso = AgregarServidor(ip, puerto);
						}
						Util.LogFormat("-> DNSProtocolCommand.NewFrontEnd. Nodo:{0}", proceso);
						ret = DNSProtocol.Encode(DNSProtocolCommand.ReponseNewFrontEnd, DNSProtocol.EncodeReponseNewFrontEnd(proceso));
						break;

					case DNSProtocolCommand.DameLista:						
						List<clsInfo> lista = ObtenerLista();
						ret = DNSProtocol.Encode(DNSProtocolCommand.ResponseDameLista,
								DNSProtocol.EncodeResponseDameLista(lista));
						break;

					case DNSProtocolCommand.DameMayores:
						datos = content.Split(new char[] { '|' });
						if (datos.Length > 0)
						{
							ip = DNSProtocol.StrToByteArray(datos[1].ToString());
							puerto = Int32.Parse(datos[0].ToString());

							cNodo = EncontrarNodo(ip, puerto);
						}
						List<clsInfo> listaMayores = ListaMayores(cNodo);
						ret = DNSProtocol.Encode(DNSProtocolCommand.ResponseDameMayores,
								DNSProtocol.EncodeResponseDameMayores(listaMayores));

						break;

					case DNSProtocolCommand.DameServidorFronEnd:
						this.SetearProximo();
						ret = DNSProtocol.Encode(DNSProtocolCommand.ResponseDameServidorFronEnd,
												 DNSProtocol.EncodeIpPuerto(cProximo.Puerto, cProximo.Ip));
						break;

					default:
						break;
				}

				try
				{
					Send(handler, ret);
					handler.BeginReceive(state.buffer, 0, DNSStateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state);				
				}
				catch (SocketException se)
				{
					Util.LogFormat("* SocketException : {0}", se.ToString());
				}
				catch (Exception e)
				{
					Util.LogFormat("* Exception : {0}", e.ToString());
				}

			}
		}



		private static void Send(Socket handler, byte[] byteData)
		{
			//byte[] byteData = Encoding.ASCII.GetBytes(data);
			handler.BeginSend(byteData, 0, byteData.Length, 0,
				new AsyncCallback(SendCallback), handler);
		}

		private static void SendCallback(IAsyncResult ar)
		{
			try
			{
				// Retrieve the socket from the state object.
				Socket handler = (Socket)ar.AsyncState;

				// Complete sending the data to the remote device.
				int bytesSent = handler.EndSend(ar);

				//handler.Shutdown(SocketShutdown.Both);
				//handler.Close();
			}
			catch (SocketException se)
			{
				Util.LogFormat("* SocketException : {0}", se.ToString());
			}
			catch (Exception e)
			{
				Util.LogFormat("* Exception : {0}", e.ToString());
			}
		}


		private clsInfo EncontrarNodo(byte[] pIp, int pPuerto)
		{
			clsInfo ret = new clsInfo();

			foreach (clsInfo cNodo in frontEnds)
			{
				if ((cNodo.Puerto == pPuerto))/*&& (cNodo.Ip == pIp)*/
				{
					ret = cNodo;
					break;
				}
			}
			return ret;
		}
	}
}
